﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace Borg
{
    public class AssemblyAnalyzer
    {
        public const string AnalyzerId = "Borg.AssemblyAnalyzer";
        private readonly TypeBuilder _typeBuilder;

        public AssemblyAnalyzer(Runtime runtime)
        {
            if (runtime == null)
                throw new ArgumentNullException("runtime");
            
            _typeBuilder = runtime.TypeBuilder;
        }

        public static bool Analyze(Uri uri, TypeBuilder builder, Uri environmentUri,
            out Contract contract, out Plugin plugin)
        {
            return Analyze(uri, uri, builder, environmentUri, out contract, out plugin);
        }

        public static bool Analyze(Uri originUri, Uri assemblyUri, TypeBuilder builder,
            Uri environmentUri, out Contract contract, out Plugin plugin)
        {
            var assembly = InternalGetAssembly(builder.Runtime, assemblyUri);

            if (!(assembly == null))
                return InternalAnalyze(originUri, assembly, builder,
                    environmentUri, out contract, out plugin);

            contract = null;
            plugin = null;

            return false;
        }

        public static bool Analyze(Assembly assembly, TypeBuilder builder,
            Uri environmentUri, out Contract contract, out Plugin plugin)
        {
            return InternalAnalyze(GetAssemblyUri(assembly), assembly,
                builder, environmentUri, out contract, out plugin);
        }

        public static bool Analyze(Uri originUri, Assembly assembly, TypeBuilder builder,
            Uri environmentUri, out Contract contract, out Plugin plugin)
        {
            return InternalAnalyze(originUri, assembly, builder,
                environmentUri, out contract, out plugin);
        }

        public bool Analyze(Uri originUri, Assembly assembly, TypeBuilder builder,
            out Contract contract, out Plugin plugin)
        {
            return InternalAnalyze(originUri, assembly, builder, out contract, out plugin);
        }

        public bool Analyze(Uri uri, TypeBuilder builder, out Contract contract, out Plugin plugin)
        {
            return Analyze(uri, uri, builder, out contract, out plugin);
        }

        public bool Analyze(Uri originUri, Uri assemblyUri, TypeBuilder builder,
            out Contract contract, out Plugin plugin)
        {
            var assembly = InternalGetAssembly(builder.Runtime, assemblyUri);

            if (!(assembly == null))
                return InternalAnalyze(originUri, assembly, builder, out contract, out plugin);

            contract = null;
            plugin = null;

            return false;
        }

        public bool Analyze(Assembly assembly, TypeBuilder builder,
            out Contract contract, out Plugin plugin)
        {
            return InternalAnalyze(GetAssemblyUri(assembly), assembly,
                builder, out contract, out plugin);
        }

        public void Analyze(IEnumerable<Uri> uris, out List<Contract> contracts,
            out List<Plugin> plugins)
        {
            if (uris == null)
                throw new ArgumentNullException("uris");
            
            InternalAnalyze(YieldAssemblies(_typeBuilder.Runtime, uris),
                out contracts, out plugins);
        }

        public void Analyze(IEnumerable<Assembly> assemblies,
            out List<Contract> contracts, out List<Plugin> plugins)
        {
            if (assemblies == null)
                throw new ArgumentNullException("assemblies");
            
            InternalAnalyze(assemblies, out contracts, out plugins);
        }

        protected static Assembly InternalGetAssembly(Runtime runtime, Uri uri)
        {
            if (!IsAssemblyUri(uri))
                return null;

            var assemblyFile = Uri.UnescapeDataString(uri.AbsoluteUri);

            try
            {
                return Assembly.ReflectionOnlyLoadFrom(assemblyFile);
            }
            catch (ReflectionTypeLoadException ex)
            {
                LogTypeLoadError(runtime, uri, ex);

                return null;
            }
        }

        protected static void LogTypeLoadError(Runtime runtime, Uri uri, ReflectionTypeLoadException e)
        {
            var builder = new StringBuilder();
            
            foreach (var exception in e.LoaderExceptions.Where(exception => exception != null))
            {
                builder.Append(Environment.NewLine);
                builder.AppendFormat("{0}: {1}", exception.GetType().Name, exception.Message);
            }

            runtime.Logger.Log(
                VerbosityLevel.Normal,
                LogType.Error,
                runtime,
                "Errors loading a type while analyzing assembly {0}: {1}",
                (object) uri, (object) builder);
        }

        protected static Uri GetAssemblyUri(Assembly assembly)
        {
            return new Uri(assembly.EscapedCodeBase);
        }

        protected static bool IsAssemblyUri(Uri uri)
        {
            if (!uri.IsFile || !File.Exists(uri.LocalPath))
                return false;

            var extension = Path.GetExtension(uri.LocalPath);

            return
                extension != null &&
                (extension.Equals(".dll", StringComparison.OrdinalIgnoreCase) ||
                extension.Equals(".exe", StringComparison.OrdinalIgnoreCase));
        }

        protected bool InternalAnalyze(Uri originUri, Assembly assembly,
            TypeBuilder builder, out Contract contract, out Plugin plugin)
        {
            return InternalAnalyze(originUri, assembly, builder,
                GetEnvironmentUri(assembly), out contract, out plugin);
        }

        protected static bool InternalAnalyze(Uri originUri, Assembly assembly, TypeBuilder builder,
            Uri environmentUri, out Contract contract, out Plugin plugin)
        {
            try
            {
                var nullable = AssemblyAnalyzerImpl.Analyze(originUri, assembly);

                if (nullable.HasValue)
                {
                    contract = BuildContract(nullable.Value, builder, environmentUri);
                    plugin = BuildPlugin(nullable.Value, builder, environmentUri);
                    
                    return true;
                }
            }
            catch (ReflectionTypeLoadException ex)
            {
                LogTypeLoadError(builder.Runtime, null, ex);
            }

            contract = null;
            plugin = null;

            return false;
        }

        protected virtual void InternalAnalyze(IEnumerable<Assembly> assemblies,
            out List<Contract> contracts, out List<Plugin> plugins)
        {
            contracts = new List<Contract>();
            plugins = new List<Plugin>();

            Func<object, AnalyzerResult> function = Analyze;

            var list = assemblies.Select(assembly =>
                Task.Factory.StartNew(function, assembly)).ToList();

            Task.WaitAll(list.ToArray());
            
            foreach (var result in list.Select(task => task.Result))
            {
                if (result.Contract != null)
                    contracts.Add(result.Contract);

                if (result.Plugin != null)
                    plugins.Add(result.Plugin);
            }
        }

        protected virtual AnalyzerResult Analyze(object assemblyObj)
        {
            var assembly = (Assembly)assemblyObj;
            Contract contract;
            Plugin plugin;

            InternalAnalyze(GetAssemblyUri(assembly), assembly, _typeBuilder,
                GetEnvironmentUri(assembly), out contract, out plugin);
            
            return new AnalyzerResult(contract, plugin);
        }

        protected virtual Uri GetEnvironmentUri(Assembly assembly)
        {
            return _typeBuilder.Runtime.EnvironmentUri;
        }

        protected static Contract BuildContract(AssemblyAnalyzerResult result,
            TypeBuilder builder, Uri environmentUri)
        {
            var slotDefs =
                result.InternalGetSlotDefinitions().Select(attribute =>
                    BuildSlotDefinition(result, attribute, builder)).ToList();

            return
                slotDefs.Count > 0
                    ? builder.CreateContract(
                        result.Name, result.Assembly,
                        environmentUri, slotDefs, "Borg.AssemblyAnalyzer")
                    : null;
        }

        protected static SlotDefinition BuildSlotDefinition(AssemblyAnalyzerResult result,
            SlotDefinitionAttribute attribute, TypeBuilder builder)
        {
            var tagDefs = new List<TagDefinition>();

            foreach (var tagDefAttr in result.GetTagDefinitions(attribute))
                tagDefs.AddRange(builder.CreateTagDefinitions(tagDefAttr));
            
            var paramDefs = result.GetParamDefinitions(attribute).
                Select(builder.CreateParamDefinition).ToList();

            return builder.CreateSlotDefinition(
                attribute, result.GetSlotDefinitionType(attribute), tagDefs, paramDefs);
        }

        protected static Plugin BuildPlugin(AssemblyAnalyzerResult result, TypeBuilder builder, Uri environmentUri)
        {
            var extTypes = result.InternalGetExtensions().
                Select(extAttr => BuildExtensionType(result, extAttr, builder)).ToList();

            return
                extTypes.Count > 0
                    ? builder.CreatePlugin(
                        result.Name, result.Assembly,
                        environmentUri, extTypes, "Borg.AssemblyAnalyzer")
                    : null;
        }

        protected static ExtensionType BuildExtensionType(AssemblyAnalyzerResult result,
            ExtensionAttribute attribute, TypeBuilder builder)
        {
            var plugTypes =
                (from plugAttr in result.InternalGetPlugs(attribute)
                 let @params =
                     (from paramAttr in result.InternalGetParameters(attribute)
                      where string.IsNullOrEmpty(paramAttr.Plug) ||
                            paramAttr.Plug.Equals(plugAttr.Name)
                      select builder.CreateParam(paramAttr)).ToList()
                 select builder.CreatePlugType(plugAttr, @params)).ToList();

            var slotTypes = result.InternalGetSlots(attribute).
                Select(builder.CreateSlotType).ToList();

            return builder.CreateExtensionType(attribute,
                result.GetExtensionTypeType(attribute), plugTypes, slotTypes);
        }

        private static IEnumerable<Assembly> YieldAssemblies(Runtime runtime, IEnumerable<Uri> uris)
        {
            return uris.Select(uri => InternalGetAssembly(runtime, uri)).Where(assembly => !(assembly == null));
        }
    }
}

